package world;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Random;
import java.util.Scanner;
import java.util.Set;

import parsing.InvalidSyntaxException;

public class CritterWorld {
	//World constants - defined in constructor
	public final int BASE_DAMAGE;
	public final double DAMAGE_INC;
	public final int ENERGY_PER_PLANT;
	public final int ENERGY_PER_SIZE;
	public final int FOOD_PER_SIZE;
	public final int MAX_SMELL_DISTANCE;
	public final int ROCK_VALUE;
	public final int MAX_COLUMN;
	public final int MAX_ROW;
	public final int MAX_RULES_PER_TURN;
	public final int PLANTS_CREATED_PER_TURN;
	public final double PLANT_GROW_PROB;
	public final int MOVE_COST;
	public final int ATTACK_COST;
	public final int GROW_COST;
	public final int BUD_COST;
	public final int MATE_COST;
	public final int RULE_COST;
	public final int ABILITY_COST;
	public final int INITIAL_ENERGY;
	
	// Names of all the constants as Strings.
	public static final String[] constantNames = {"BASE_DAMAGE","DAMAGE_INC","ENERGY_PER_PLANT","ENERGY_PER_SIZE",
		"FOOD_PER_SIZE","MAX_SMELL_DISTANCE","ROCK_VALUE","MAX_COLUMN","MAX_ROW","MAX_RULES_PER_TURN","PLANTS_CREATED_PER_TURN",
		"PLANT_GROW_PROB","MOVE_COST","ATTACK_COST","GROW_COST","BUD_COST","MATE_COST","RULE_COST","ABILITY_COST","INITIAL_ENERGY"};
	
	public final int ROW_COL_EXPR;//(2*MAX_ROW - MAX_COLUMN) for odd MAX_COLUMN
								  //(2*(MAX_ROW+1) - (MAX_COLUMN+1) for even MAX_COLUMN
	public final double ROCK_PROB = 0.08;
	public final double CRITTER_PROB = 0.12;
	public final int CRITTER_MAX_OFF = 15;
	public final int CRITTER_MAX_DEF = 15;
	
	//contents of world
	private HashMap<Location, Hex> grid;
	private Set<Hex> plantHexes;
	private List<Critter> critters;
	private int numRocks;
	private int time; // Number of timesteps taken.
	
	//to eliminate redundant instantiations
	private Random rnd;
	public final int ROWS_PER_COL;
	
	public int getTime() {
		return time;
	}
	
	public int getNumPlants() {
		return plantHexes.size();
	}
	
	public int getNumCritters() {
		return critters.size();
	}

	/**
	 * Creates a new CritterWorld based on the constants defined in the file specified,
	 * and the critters, rocks, and plants in the world file specified.
	 * Checks: the constants file is formatted such that every line has:
	 * *constant_name* *value* *comments*
	 * @param constantsFileName the name of the file containing the constants
	 * @param worldFileName the world file containing critters, rocks, and plants.
	 * 				If null, randomly generates rocks, plants, and critters.
	 * @param allValid when the constructor is done, this Boolean will be true if all elements
	 * 				were successfully added to the world
	 * @throws IOException if either of the files are not found, any constants are absent form
	 * 				the constants file, or either of the files are formatted incorrectly.
	 * @throws InvalidSyntaxException, IllegalArgumentException if one of the critters in the
	 * 				world file was an invalid critter.
	 * @throws LoadIncompleteException if one or more of the world components was not created.
	 */
	public CritterWorld(String constantsFileName, String worldFileName, final LoadState state) 
					throws IOException, InvalidSyntaxException,	IllegalArgumentException{
		Scanner constantsInput = new Scanner(new File(constantsFileName));
		ArrayList<String> names = new ArrayList<String>();
		ArrayList<Double> values = new ArrayList<Double>();
		while (constantsInput.hasNextLine()){//get all of the names and values from the file
			try {
				names.add(constantsInput.next());
				values.add(constantsInput.nextDouble());
				constantsInput.nextLine();
			} catch (NoSuchElementException e) {
				throw new IOException("Constants file must have *constant_name* *value* *comments* on each line");
			}
		}
		
		int index;
		HashMap<String,Double> constants = new HashMap<String, Double>();
		for(int i = 0; i < constantNames.length; i++){//map the constant names to their values
			index = names.indexOf(constantNames[i]);
			if (index < 0){
				throw new IOException("Constant " + constantNames[i] + " was not in " + constantsFileName);
			}
			constants.put(constantNames[i], values.get(index));
		}
		
		BASE_DAMAGE = constants.get("BASE_DAMAGE").intValue();
		DAMAGE_INC = constants.get("DAMAGE_INC").doubleValue();
		ENERGY_PER_PLANT = constants.get("ENERGY_PER_PLANT").intValue();
		ENERGY_PER_SIZE = constants.get("ENERGY_PER_SIZE").intValue();
		FOOD_PER_SIZE = constants.get("FOOD_PER_SIZE").intValue();
		MAX_SMELL_DISTANCE = constants.get("MAX_SMELL_DISTANCE").intValue();
		ROCK_VALUE = constants.get("ROCK_VALUE").intValue();
		MAX_COLUMN = constants.get("MAX_COLUMN").intValue();
		MAX_ROW = constants.get("MAX_ROW").intValue();
		MAX_RULES_PER_TURN = constants.get("MAX_RULES_PER_TURN").intValue();
		PLANTS_CREATED_PER_TURN = constants.get("PLANTS_CREATED_PER_TURN").intValue();
		PLANT_GROW_PROB = constants.get("PLANT_GROW_PROB").doubleValue();
		MOVE_COST = constants.get("MOVE_COST").intValue();
		ATTACK_COST = constants.get("ATTACK_COST").intValue();
		GROW_COST = constants.get("GROW_COST").intValue();
		BUD_COST = constants.get("BUD_COST").intValue();
		MATE_COST = constants.get("MATE_COST").intValue();
		RULE_COST = constants.get("RULE_COST").intValue();
		ABILITY_COST = constants.get("ABILITY_COST").intValue();
		INITIAL_ENERGY = constants.get("INITIAL_ENERGY").intValue();
		
		ROW_COL_EXPR = (MAX_COLUMN % 2 == 0) ? (2*(MAX_ROW+1) - (MAX_COLUMN+1)) : 
												(2*MAX_ROW - MAX_COLUMN);
		
		grid = new HashMap<Location, Hex>();
		plantHexes = new HashSet<Hex>();
		critters = new LinkedList<Critter>();
		numRocks = 0;
		rnd = new Random();
		ROWS_PER_COL = (2*MAX_ROW - MAX_COLUMN)/2 + 1;
		time = 0;

		if (worldFileName == null) {
			for(int c = 0; c <= MAX_COLUMN; c++){
				int endRow = (c + ROW_COL_EXPR)/2;
				for(int r = (c+1)/2; r <= endRow; r++){
					double probVal = Math.random();
					if(probVal <= ROCK_PROB){
						getHex(new Location(this,r,c)).makeRock();
						numRocks++;
					}
					if(probVal <= ROCK_PROB + CRITTER_PROB && probVal > ROCK_PROB){
						Hex critHex = getHex(new Location(this,r,c));
						int offense = rnd.nextInt(CRITTER_MAX_OFF) + 1;
						int defense = rnd.nextInt(CRITTER_MAX_DEF) + 1;
						critHex.putCritter(new Critter(this, critHex, 7, defense, offense));
					}
				}
			}
		} else {
			Scanner worldScanner = new Scanner(new File(worldFileName));
			try {
				while (worldScanner.hasNext()) {
					String lineIdentifier = worldScanner.next();
					if ("plant".equals(lineIdentifier)) {
						int row = Integer.parseInt(worldScanner.next());
						int col = Integer.parseInt(worldScanner.next());
						Hex plantHex = getHex(new Location(this, row, col));
						if (plantHex == null || !plantHex.putPlant())
							state.failed();
					} else if ("rock".equals(lineIdentifier)) {
						int row = Integer.parseInt(worldScanner.next());
						int col = Integer.parseInt(worldScanner.next());
						Hex rockHex = getHex(new Location(this, row, col));
						if (rockHex == null || !rockHex.makeRock())
							state.failed();
					} else if ("critter".equals(lineIdentifier)) {
						String genomeFileName = worldScanner.next();
						int row = Integer.parseInt(worldScanner.next());
						int col = Integer.parseInt(worldScanner.next());
						int direction = Integer.parseInt(worldScanner.next());
						Hex critterHex = getHex(new Location(this, row, col));
						if (critterHex == null || critterHex.isOccupied())
							state.failed();
						else {
							try {
								new Critter(this, critterHex, direction, genomeFileName);
							} catch (InvalidSyntaxException e) {
								throw new InvalidSyntaxException("The critter file:\n" + genomeFileName +
															   "\nhas an invalid syntax.");
							}
						}
					} else {
						throw new IOException("Each line of World file:\n" + worldFileName + 
								"\nmust start with the word \"plant\", \"rock\", or \"critter\"");
					}
				}
			} catch (NumberFormatException e) {
				throw new IOException("World file:\n" + worldFileName + "\nwas not formatted correctly.");
			}
		}
	}
	

	 /** 
	  * Creates a new CritterWorld based on the constants defined in the file specified
	  * Randomly assigns Critters and Rocks to the world
	  * Checks: the constants file is formatted such that every line has:
	  * *constant_name* *value* *comments*
	  * @param constantsFileName the name of the file containing the constants
	  * @throws IOException if the constants file is not found, any constants are absent form
	  * 		the constants file, or the constants file is formatted incorrectly.
	  */
	public CritterWorld(String constantsFileName) throws IOException {
		this(constantsFileName, null, null);
	}
	
	/**
	 * Gets the Hex corresponding to the given Location.
	 * @param loc the Location to look up.
	 * @return the corresponding Hex. Null if this Location is not valid.
	 */
	public Hex getHex(Location loc) {
		if (!loc.isValid()) return null;
		Hex rtn = grid.get(loc);
		if (rtn == null) {
			rtn = new Hex(this, loc);
			grid.put(loc, rtn);
		}
		return rtn;
	}
	
	/**
	 * Adds the specified Hex to the set of critter hexes.
	 * Checks: The provided Hex has a critter.
	 * @param h the Hex to be added to the list of Hexes with Critters.
	 * @return false if the given Hex was already in the set.
	 */
	public boolean putCritter(Critter c){
		return critters.add(c);
	}
	
	/**
	 * Removes the given Hex from the critterHexes set if it was there.
	 * @param h the Hex to be removed.
	 */
	public void removeCritter(Critter c){
		critters.remove(c);
	}
	
	/**
	 * Adds the specified Hex to the set of plant hexes.
	 * Checks: The provided Hex has a plant.
	 * @param h the Hex to be added to the list of Hexes with Plants.
	 * @return false if the given Hex was already in the set.
	 */
	public boolean putPlantHex(Hex h){
		if (!h.hasPlant())
			throw new IllegalArgumentException("putPlantHex only accepts Hexes with plants as a parameter.");
		return plantHexes.add(h);
	}
	
	/**
	 * Removes the given Hex from the plantHexes set if it was there.
	 * @param h the Hex to be removed.
	 */
	public void removePlantHex(Hex h){
		plantHexes.remove(h);
	}
	
	public void incrementRocks(){
		numRocks++;
	}
	
	/**
	 * Moves the world forward one timestep and updates the world's status and contents as needed.
	 * @param allWait if true, all Critters will wait for their action. If false, all Critters
	 * will act according to their genome
	 */
	public void incrementTimestep(boolean allWait) {
		incrementTimestep(allWait, null, 0);
	}
	
	/**
	 * Moves the world forward one timestep, with the Critter in the location specified taking
	 * the action specified, and updates the world's status and contents as needed.
	 * If there is no Critter in the Location given, or it is a null pointer, the world progresses
	 * without any Critters being manually controlled.
	 * @param allWait if true, all Critters will wait for their action. If false, all Critters
	 * will act according to their genome
	 * @param controlLoc the Location containing the Critter to be controlled
	 * @param action the action for the Critter to take. Must be one of the command constants
	 * defined in the Critter class.
	 */
	public void incrementTimestep(boolean allWait, Critter controlCrit, int action) {
		int numCritters = Math.max(critters.size(), 1);//so world advances after all critters are dead
		int[] plantsSpawnedOnTurn = new int[numCritters];
		for(int i = 0; i < PLANTS_CREATED_PER_TURN; i++)//pick turns to spawn a plant on
			plantsSpawnedOnTurn[rnd.nextInt(numCritters)]++;
		int turnsComplete = 0;
		LinkedList<Critter> critList = new LinkedList<Critter>(critters);
		LinkedList<Hex> plantHexList = new LinkedList<Hex>(plantHexes);
		for(Hex h : plantHexList)
			h.getReady();
		for(Critter c : critList)
			c.getReady();
		double practicalGrowProb = 1 - Math.pow( 1 - PLANT_GROW_PROB, 1.0/(numCritters + 1));
		for (Hex pHex : plantHexList) {
			double plantGrowVal = Math.random();
			if (plantGrowVal < practicalGrowProb)
				pHex.sproutPlant();
		}
		for(Critter c : critList){
			if(c.equals(controlCrit))
				c.takeSpecifiedAction(action);
			else if(allWait)
				c.rest();
			else
				c.act();
			for (Hex pHex : plantHexList) {
				double plantGrowVal = Math.random();
				if (plantGrowVal < practicalGrowProb)
					pHex.sproutPlant();
			}
			for(int i = 0; i < plantsSpawnedOnTurn[turnsComplete]; i++)
				spawnPlant();
			turnsComplete++;
		}		
		time++;
	}
	
	/**
	 * Returns a randomly selected Hex that does not contain a Critter or a rock
	 * unless every Hex is occupied. Then returns a randomly selected occupied Hex.
	 */
	public Hex getRandomEmptyHex(){
		int row, col;
		Hex rtn;
		do{
			col = rnd.nextInt(MAX_COLUMN + 1);
			row = rnd.nextInt(ROWS_PER_COL) + (col+1)/2;
			rtn = getHex(new Location(this, row, col));
		} while(rtn.isOccupied() && ((critters.size() + numRocks) < (ROWS_PER_COL * (MAX_COLUMN + 1))));
		return rtn;
	}
	
	/**
	 * Randomly selects a Hex until one without a plant is found and puts a plant there
	 */
	private void spawnPlant(){
		int row, col;
		Hex garden;
		do{
			col = rnd.nextInt(MAX_COLUMN + 1);
			row = rnd.nextInt(ROWS_PER_COL) + (col+1)/2;
			garden = getHex(new Location(this, row, col));
		} while((garden.hasPlant() || garden.isRock()) && ((plantHexes.size() + numRocks) < (ROWS_PER_COL * (MAX_COLUMN + 1))));
		garden.putPlant();
	}
}